package com.googlecode.cswish.struts.spring;

import java.io.IOException;
import java.lang.annotation.Annotation;
import java.net.URLClassLoader;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashSet;
import java.util.List;
import java.util.Locale;
import java.util.Map;

import org.springframework.beans.BeansException;
import org.springframework.beans.factory.BeanFactory;
import org.springframework.beans.factory.NoSuchBeanDefinitionException;
import org.springframework.beans.factory.NoUniqueBeanDefinitionException;
import org.springframework.beans.factory.config.AutowireCapableBeanFactory;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationEvent;
import org.springframework.context.MessageSourceResolvable;
import org.springframework.context.NoSuchMessageException;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.annotation.ClassPathBeanDefinitionScanner;
import org.springframework.core.env.Environment;
import org.springframework.core.io.Resource;
import org.springframework.core.type.filter.AnnotationTypeFilter;
import org.springframework.stereotype.Component;
import org.springframework.stereotype.Service;

import com.googlecode.cswish.struts.ContextHolder;
import com.googlecode.cswish.struts.hibernate.DynamicClassLoaderFactory;
import com.googlecode.cswish.struts.hibernate.SiteClassLoader;
import com.googlecode.cswish.struts.spring.CacheService.Wrapper;

/**
 * FIXME: only use siteContext? it extends the parent
 * 
 * 
 * @author Jerry
 *
 */
public class SiteApplicationContext implements ApplicationContext {
	
	private ApplicationContext parent;
	
	// it should based on the base product of the site
	private Wrapper<ApplicationContext> siteContext;
	
	private static SiteApplicationContext instance;
	
	public final static synchronized SiteApplicationContext getInstance(ApplicationContext parent) {
		if (instance == null) {
			instance = new SiteApplicationContext(parent);
		}
		return instance;
	}
	
	private SiteApplicationContext(ApplicationContext parent) {
		this.parent = parent;
		CacheService cacheService = parent.getBean(CacheService.class);
		this.siteContext = cacheService.newVarWithCreator(ApplicationContext.class, new Executor<ApplicationContext, String>() {
			@Override
			public ApplicationContext execute(String key) {
				ApplicationContext appContext = create3rdLibAppContext();
				return appContext;
			}
		});
	}
	
	private ApplicationContext create3rdLibAppContext() {
		ClassLoader classLoader = Thread.currentThread().getContextClassLoader();
		SiteClassLoader siteClassLoader = null;
		if (classLoader instanceof SiteClassLoader) {
			siteClassLoader = (SiteClassLoader) classLoader;
		}
		
		ApplicationContext appContext;
		boolean needSwitchClassLoader = siteClassLoader == null;
		// switch the class loader to SiteClassLoader
		try {
			if (needSwitchClassLoader) {
				DynamicClassLoaderFactory dynamicClassLoaderFactory = parent.getBean(DynamicClassLoaderFactory.class);
				dynamicClassLoaderFactory.bindSiteClassLoader();
				siteClassLoader = (SiteClassLoader) Thread.currentThread().getContextClassLoader();
			}
			
			URLClassLoader thirdLibNoParent = siteClassLoader.getLibClassLoaderNoParent();

			// cacheService close the resource automatically
			AnnotationBeanNameGenerator beanNameGenerator = new AnnotationBeanNameGenerator();
			AnnotationConfigApplicationContext annoAppContext = new AnnotationConfigApplicationContext();
			annoAppContext.setClassLoader(thirdLibNoParent);
			annoAppContext.setBeanNameGenerator(beanNameGenerator);

			ClassPathBeanDefinitionScanner scanner = new ClassPathBeanDefinitionScanner(annoAppContext, false);
			scanner.addIncludeFilter(new AnnotationTypeFilter(Service.class));
			scanner.addIncludeFilter(new AnnotationTypeFilter(Component.class));
			scanner.setBeanNameGenerator(beanNameGenerator);
			scanner.scan("com");
			annoAppContext.setClassLoader(siteClassLoader.getLibClassLoader());
			annoAppContext.setParent(parent);
			annoAppContext.refresh();

			appContext = annoAppContext;
		} finally {
			if (needSwitchClassLoader) {
				DynamicClassLoaderFactory dynamicClassLoaderFactory = parent.getBean(DynamicClassLoaderFactory.class);
				dynamicClassLoaderFactory.unbindSiteClassLoader();
			}
		}
		return appContext;
	}
	
	public void refresh() {
		DynamicClassLoaderFactory dynamicClassLoaderFactory = parent.getBean(DynamicClassLoaderFactory.class);
		dynamicClassLoaderFactory.reloadSiteClassLoader(ContextHolder.get().getSiteName());
		// create a new application and bind it to 'siteContext'
		ApplicationContext appContext = create3rdLibAppContext();
		siteContext.set(appContext);
	}
	
	@Override
	public void publishEvent(ApplicationEvent event) {
		parent.publishEvent(event);
		siteContext.get().publishEvent(event);
	}

	@Override
	public BeanFactory getParentBeanFactory() {
		return parent.getParentBeanFactory();
	}

	@Override
	public boolean containsLocalBean(String name) {
		return parent.containsLocalBean(name) || siteContext.get().containsLocalBean(name);
	}

	@Override
	public String getMessage(String code, Object[] args, String defaultMessage,
			Locale locale) {
		String message = parent.getMessage(code, args, defaultMessage, locale);
		if (message == null) {
			message = siteContext.get().getMessage(code, args, defaultMessage, locale);
		}
		return message;
	}

	@Override
	public Resource getResource(String location) {
		Resource resource = parent.getResource(location);
		if (resource == null) {
			resource = siteContext.get().getResource(location);
		}
		return resource;
	}

	@Override
	public Environment getEnvironment() {
		return parent.getEnvironment();
	}

	@Override
	public String getMessage(String code, Object[] args, Locale locale)
			throws NoSuchMessageException {
		String message = parent.getMessage(code, args, locale);
		if (message != null) {
			message = siteContext.get().getMessage(code, args, locale);
		}
		return message;
	}

	@Override
	public boolean containsBeanDefinition(String beanName) {
		return parent.containsBeanDefinition(beanName) 
				|| siteContext.get().containsBeanDefinition(beanName);
	}

	@Override
	public ClassLoader getClassLoader() {
		return siteContext.get().getClassLoader();
	}

	@Override
	public String getId() {
		return parent.getId();
	}

	@Override
	public Resource[] getResources(String locationPattern) throws IOException {
		Resource[] resources = parent.getResources(locationPattern);
		Resource[] siteResources = siteContext.get().getResources(locationPattern);
		
		List<Resource> mergeResources = new ArrayList<>(resources.length + siteResources.length);
		for (Resource resource : resources) {
			mergeResources.add(resource);
		}
		for (Resource resource : siteResources) {
			mergeResources.add(resource);
		}
		return mergeResources.toArray(new Resource[mergeResources.size()]);
	}

	@Override
	public String getApplicationName() {
		return parent.getApplicationName();
	}
	
	@Override
	public String getMessage(MessageSourceResolvable resolvable, Locale locale)
			throws NoSuchMessageException {
		String message = parent.getMessage(resolvable, locale);
		if (message == null) {
			message = siteContext.get().getMessage(resolvable, locale);
		}
		return message;
	}

	@Override
	public int getBeanDefinitionCount() {
		return parent.getBeanDefinitionCount() + siteContext.get().getBeanDefinitionCount();
	}

	@Override
	public String getDisplayName() {
		return parent.getDisplayName();
	}

	@Override
	public long getStartupDate() {
		return parent.getStartupDate();
	}

	@Override
	public String[] getBeanDefinitionNames() {
		String[] names = parent.getBeanDefinitionNames();
		String[] siteNames = siteContext.get().getBeanDefinitionNames();
		
		return mergeNames(names, siteNames);
	}

	@Override
	public ApplicationContext getParent() {
		return parent;
	}

	@Override
	public AutowireCapableBeanFactory getAutowireCapableBeanFactory()
			throws IllegalStateException {
		return parent.getAutowireCapableBeanFactory();
	}

	@Override
	public String[] getBeanNamesForType(Class<?> type) {
		String[] names = parent.getBeanNamesForType(type);
		String[] siteNames = siteContext.get().getBeanNamesForType(type);
		
		return mergeNames(names, siteNames);
	}

	private String[] mergeNames(String[] names, String[] siteNames) {
		if (siteNames == null || siteNames.length == 0) {
			return names;
		} else if (names == null || names.length == 0) {
			return siteNames;
		}
		
		Collection<String> mergeNames = new HashSet<>(names.length + siteNames.length);
		for (String name : names) {
			mergeNames.add(name);
		}
		for (String name : siteNames) {
			mergeNames.add(name);
		}
		return mergeNames.toArray(new String[mergeNames.size()]);
	}

	@Override
	public String[] getBeanNamesForType(Class<?> type,
			boolean includeNonSingletons, boolean allowEagerInit) {
		String[] names = parent.getBeanNamesForType(type, includeNonSingletons,
				allowEagerInit);
		String[] siteNames = siteContext.get().getBeanNamesForType(type, includeNonSingletons,
				allowEagerInit);
		return mergeNames(names, siteNames);
	}

	@Override
	public Object getBean(String name) throws BeansException {
		if (parent.containsBean(name)) {
			return parent.getBean(name);
		} else {
			return siteContext.get().getBean(name);
		}
	}

	
	@Override
	public <T> T getBean(String name, Class<T> requiredType)
			throws BeansException {
		if (parent.containsBean(name)) {
			return parent.getBean(name, requiredType);
		} else {
			return siteContext.get().getBean(name, requiredType);
		}
	}

	@Override
	public <T> Map<String, T> getBeansOfType(Class<T> type)
			throws BeansException {
		Map<String, T> beans = parent.getBeansOfType(type);
		Map<String, T> siteBans = siteContext.get().getBeansOfType(type);
		beans.putAll(siteBans);
		return beans;
	}

	@Override
	public <T> T getBean(Class<T> requiredType) throws BeansException {
		String[] beanNames = this.getBeanNamesForType(requiredType);
		if (beanNames.length == 1) {
			return getBean(beanNames[0], requiredType);
		}
		else if (beanNames.length > 1) {
			throw new NoUniqueBeanDefinitionException(requiredType, beanNames);
		}
		else {
			throw new NoSuchBeanDefinitionException(requiredType);
		}
	}

	@Override
	public Object getBean(String name, Object... args) throws BeansException {
		if (parent.containsBean(name)) {
			return parent.getBean(name, args);
		} else {
			return siteContext.get().getBean(name, args);
		}
	}

	@Override
	public <T> Map<String, T> getBeansOfType(Class<T> type,
			boolean includeNonSingletons, boolean allowEagerInit)
			throws BeansException {
		Map<String, T> beans = parent
				.getBeansOfType(type, includeNonSingletons, allowEagerInit);
		Map<String, T> siteBeans = siteContext.get()
					.getBeansOfType(type, includeNonSingletons, allowEagerInit);
		beans.putAll(siteBeans);
		return beans;
	}

	@Override
	public boolean containsBean(String name) {
		return parent.containsBean(name) || siteContext.get().containsBean(name);
	}

	@Override
	public boolean isSingleton(String name)
			throws NoSuchBeanDefinitionException {
		if (parent.containsBean(name)) {
			return parent.isSingleton(name);
		} else {
			return siteContext.get().isSingleton(name);
		}
	}

	@Override
	public boolean isPrototype(String name)
			throws NoSuchBeanDefinitionException {
		if (parent.containsBean(name)) {
			return parent.isPrototype(name);
		} else {
			return siteContext.get().isPrototype(name);
		}
	}

	@Override
	public Map<String, Object> getBeansWithAnnotation(
			Class<? extends Annotation> annotationType) throws BeansException {
		Map<String, Object> beans = parent.getBeansWithAnnotation(annotationType);
		if (beans == null) {
			beans = siteContext.get().getBeansWithAnnotation(annotationType);
		}
		return beans;
	}

	@Override
	public <A extends Annotation> A findAnnotationOnBean(String beanName,
			Class<A> annotationType) {
		A bean = parent.findAnnotationOnBean(beanName, annotationType);
		if (bean == null) {
			bean = siteContext.get().findAnnotationOnBean(beanName, annotationType);
		}
		return bean;
	}

	@Override
	public boolean isTypeMatch(String name, Class<?> targetType)
			throws NoSuchBeanDefinitionException {
		if (parent.containsBean(name)) {
			return parent.isTypeMatch(name, targetType);
		} else {
			return siteContext.get().isTypeMatch(name, targetType);
		}
	}

	@Override
	public Class<?> getType(String name) throws NoSuchBeanDefinitionException {
		if (parent.containsBean(name)) {
			return parent.getType(name);
		} else {
			return siteContext.get().getType(name);
		}
	}

	@Override
	public String[] getAliases(String name) {
		String[] names = parent.getAliases(name);
		String[] siteNames = siteContext.get().getAliases(name);
		return mergeNames(names, siteNames);
	}
}